# Created by 黄景涛
# DATE: 2024/11/5

from selenium.webdriver import ActionChains
from selenium.webdriver.common.by import By
from selenium.common.exceptions import NoSuchElementException, TimeoutException
from selenium.webdriver.remote.webelement import WebElement
from selenium.webdriver.common.keys import Keys

from base.kdt_lib.local_mixin import LocalMiXin
from config.settings import ROOT_PATH
from utils.log_helper.logger import logger
from utils.screenshot.picture_tool import save_png


class BasicComponent(LocalMiXin):
    """ 基础组件 """

    input_loc = None
    clear_icon_loc = None

    def hover(self, input_wrapper: WebElement):
        """ 通用方法：hover至输入框 """
        actions = ActionChains(self.driver)
        actions.move_to_element(input_wrapper).perform()

    def clear(self, input_wrapper: WebElement, clear_icon_loc):
        """ 清空输入框 """
        try:
            self.hover(input_wrapper)
            clear_icon = input_wrapper.find_element(By.XPATH, clear_icon_loc)
            clear_icon.click()
        except NoSuchElementException:
            logger.info('值本来就是空的，无需清空')
            pass

    # 收起下拉框
    def _back_up(self, select_ele):
        """ 收起下拉 """
        # 获取WebElement实例的宽、高
        width, height = select_ele.rect['width'], select_ele.rect['height']
        # 余量，单位：px，起到微调作用
        margin = 1
        offset_xy = (-(width // 2 + margin), (height // 2 + margin))
        actions = ActionChains(self.driver)
        # 光标移动到下拉框的左上角，然后单击
        actions.move_to_element_with_offset(select_ele, *offset_xy).click().perform()

    @classmethod
    def check_class_val(cls, element: WebElement, val) -> bool:
        """ class中是否有val值 """
        is_exist = element.get_attribute("class").find(val) != -1
        return is_exist


class AdvancedComponent(BasicComponent):
    """ 高级组件 """


# ==================================================================================================================
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 基础字段 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<


class Text(BasicComponent):
    """ 单行文本 """
    input_loc = './/div[1]/input'
    clear_icon_loc = ".//div[2]/i"

    def input_box(self, input_wrapper: WebElement, content=None):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param content: 输入内容
        :return:
        """
        input_ = input_wrapper.find_element(By.XPATH, self.input_loc)

        # 先尝试清空值
        self.clear(input_wrapper, self.clear_icon_loc)

        if content is not None:
            input_.send_keys(content)

    # 动作：输入并回车查询
    def input_and_enter(self, input_wrapper: WebElement, content=None):
        input_ = input_wrapper.find_element(By.XPATH, self.input_loc)

        # 先尝试清空值
        self.clear(input_wrapper, self.clear_icon_loc)

        if content is not None:
            input_.send_keys(content + Keys.ENTER)


class Textarea(Text):
    """ 多行文本 """
    input_loc = ".//div[1]/textarea"


class Number(BasicComponent):
    """ 数值 """
    input_loc = ".//div/input"

    def input_box(self, input_wrapper: WebElement, content=None):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param content:  输入内容
        :return:
        """
        input_ = input_wrapper.find_element(By.XPATH, self.input_loc)

        if content is not None:
            input_.send_keys(content)


class Radio(BasicComponent):
    """ 单选框 """

    # 组件最小原子
    input_loc = './/div[contains(@class, "bk-form-radio-group")]'

    def input_box(self, input_wrapper: WebElement, option=None):
        """
        :param input_wrapper:  包囊组件的父级WebElement
        :param option: 选项，eg: 选项1； eg: 1
        :return:
        """
        radio_group_ele = input_wrapper.find_element(By.XPATH, self.input_loc)
        lis = radio_group_ele.find_elements(By.TAG_NAME, 'label')

        try:
            lis[int(option) - 1].click()
        except IndexError:
            raise IndexError(f'选项位置索引{option}越界')
        except ValueError:
            for li in lis:
                if option in li.text:
                    li.click()
                    break
            else:
                raise Exception(f'选项【{option}】不存在')


class Checkbox(BasicComponent):
    """ 复选框 """

    # 组件最小原子
    input_loc = './/div[contains(@name, "bk-checkbox-group")]'

    def input_box(self, input_wrapper: WebElement, options=None):
        """
        :param self:
        :param input_wrapper: 包囊组件的父级WebElement
        :param options: 多个选项, e.g: 1,2,4   e.g:选项1，选项2
        :return:
        """
        checkbox_group_ele = input_wrapper.find_element(By.XPATH, self.input_loc)
        lis = checkbox_group_ele.find_elements(By.TAG_NAME, 'label')

        if options.strip() == 'all':
            target_lis = lis
        else:
            options = options.strip().replace('，', ',').split(',')
            try:
                idxs = [int(idx) - 1 for idx in options]
                target_lis = [lis[i] for i in idxs]
            except ValueError:
                option_names = [name.strip() for name in options]
                target_lis = []
                for name in option_names:
                    for li in lis:
                        if name in li.text:
                            target_lis.append(li)
                            break
                    else:
                        logger.info(f"选项【{name}】未找到，跳过勾选")

        for li in target_lis:
            if self.check_class_val(li, 'is-checked'):
                logger.info(f'{li.text}已被勾选，忽略')
                continue
            li.click()


class Select(BasicComponent):
    """ 单选下拉 """

    # 选项集面板
    options_ul_loc = '//div[@class="bk-options-wrapper"]/ul'
    # 搜索框
    search_input_loc = '//div[@class="bk-select-search-wrapper"]/input'
    # 清空icon
    clear_icon_loc = './/span[contains(@class, "bk-select-clear")]'

    def input_box(self, input_wrapper: WebElement, option=None, instance=None, auto_unfold=False):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param option: 选项，支持：选项名称、位置
        :param instance: 所在的Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """
        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()
        # 截屏并保存PNG
        save_png(instance)

        ul_ele = self.find_element(self.options_ul_loc)
        lis = ul_ele.find_elements(By.TAG_NAME, 'li')

        if not len(lis):
            null_error = "搜索结果为空，或暂无选项"
            raise Exception(null_error)

        try:
            lis[int(option) - 1].click()
        except IndexError:
            raise IndexError(f'选项索引【{option}】越界')
        except ValueError:
            for li in lis:
                if option in li.text:
                    li.click()
                    break
            else:
                raise Exception(f'选项【{option}】不存在')
        except:
            raise
        finally:
            self._back_up(input_wrapper)


class MultiSelect(Select):
    """ 多选下拉 """

    # 勾选框
    # check_loc = './/label[@class="bk-form-checkbox"]'

    def input_box(self, input_wrapper: WebElement, options: str = None, instance=None, auto_unfold=False):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param options: 多个选项，支持：选项名称、位置。 eg: 选项1,选项2； eg: 1,2
        :param instance: Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """

        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()
            # self.key_sleep(0.5)
        # 截屏并保存PNG
        save_png(instance)

        options_ul_ele = self.driver.find_element(By.XPATH, self.options_ul_loc)
        lis = options_ul_ele.find_elements(By.TAG_NAME, 'li')

        try:
            if options.strip() == 'all':
                target_lis = lis
            else:
                options = options.strip().replace('，', ',').split(',')
                try:
                    idxs = [int(idx) - 1 for idx in options]
                    target_lis = [lis[i] for i in idxs]
                except ValueError:
                    option_names = [name.strip() for name in options]
                    target_lis = []
                    for name in option_names:
                        for li in lis:
                            if name in li.text:
                                target_lis.append(li)
                                break
                        else:
                            not_find_msg = f"【{name}】选项未找到，跳过勾选"
                            logger.info(not_find_msg)

            for li in target_lis:
                if self.check_class_val(li, 'is-selected'):
                    logger.info(f'{li.text}已被勾选，忽略')
                    continue
                # li.find_element(By.XPATH, self.check_loc).click()
                li.click()
                # 截屏并保存PNG
                save_png(instance)
        except:
            raise
        finally:
            self._back_up(input_wrapper)


class User(Select):
    """ 单选人员 """

    def input_box(self, input_wrapper: WebElement, option=None, search_for: str = None, sleep_sec=1, instance=None,
                  auto_unfold=False):
        """
        :param self:
        :param input_wrapper: 包囊组件的父级WebElement
        :param option: 目标选项，e.g: 人员名称、索引
        :param search_for: 搜索关键字
        :param sleep_sec: 搜索后等待
        :param instance: 所在的Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """
        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()

        if search_for is not None:
            self.search(search_for, sleep_sec)

        super().input_box(input_wrapper, option, instance=instance, auto_unfold=True)

    def search(self, search_for: str, sleep_sec: float):
        self.key_sleep(sleep_sec)
        # 内部搜索框对象
        search_input_ele = self.driver.find_element(By.XPATH, self.search_input_loc)
        search_input_ele.send_keys(search_for)
        self.key_sleep(sleep_sec)


class MultiUser(MultiSelect):
    """ 多选人员"""

    def input_box(self, input_wrapper: WebElement, options=None, search_for: str = None, sleep_sec=1, instance=None,
                  auto_unfold=False):
        """
        :param self:
        :param input_wrapper: 包囊组件的父级WebElement
        :param options: 多个目标选项，eg: 人员1,人员2； eg: 1,2
        :param search_for: 搜索关键字
        :param sleep_sec: 搜索后等待
        :param instance: 所在的Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """
        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()

        if search_for is not None:
            self.search(search_for, sleep_sec)

        super().input_box(input_wrapper, options, instance=instance, auto_unfold=True)

    def search(self, search_for: str, sleep_sec: float):
        self.key_sleep(sleep_sec)
        # 内部搜索框对象
        search_input_ele = self.driver.find_element(By.XPATH, self.search_input_loc)
        search_input_ele.send_keys(search_for)
        self.key_sleep(sleep_sec)


class Cascade(BasicComponent):
    """ 单选级联 """

    # 级联搜索框
    search_input_loc = './/div[@class="bk-cascade-name"]/input'
    # 级联ul选项集
    ul_loc = '//div[@class="bk-cascade-panel"]/ul[@class="bk-cascade-panel-ul"]'

    # 级联层级结构
    panels_wrap_loc = '//*[@class="bk-cascade-options"]'
    panel_structure = '/div[@class="bk-cascade-panel"]'

    def input_box(self, input_wrapper: WebElement, option: str, search_for: str = None, instance=None,
                  auto_unfold=False):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param option: 选项，支持两种格式：a.数字；b.具体选项路径，如：陆路>汽车类>卡车
        :param search_for: 搜索关键字
        :param instance: Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """
        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()
        # 截屏并保存PNG
        save_png(instance)

        try:
            if search_for is not None:
                search_input_ele = input_wrapper.find_element(By.XPATH, self.search_input_loc)
                search_input_ele.send_keys(search_for)
                ul_ele = self.driver.find_element(By.XPATH, self.ul_loc)
                lis = ul_ele.find_elements(By.TAG_NAME, 'li')
                lis[int(option) - 1].click()
            else:
                option_parser = [node.strip() for node in option.split('>')]
                for level, op_name in enumerate(option_parser, 1):
                    # 拼接出每级panel的ul
                    current_ul_loc = self.panels_wrap_loc + self.panel_structure * level + '/ul'
                    ul_ele = self.driver.find_element(By.XPATH, current_ul_loc)
                    current_li = [li for li in ul_ele.find_elements(By.TAG_NAME, 'li') if op_name in li.text][0]
                    current_li.click()
        except:
            raise
        finally:
            self._back_up(input_wrapper)


class MultiCascade(Cascade):
    """ 多选级联 """

    # 级联选框中，所有的子元素位置
    # 勾选框
    check_loc = './/div[@class="bk-cascade-check"]'

    def input_box(self, input_wrapper: WebElement, options: str, search_for: str = None, instance=None,
                  auto_unfold=False):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param options: 多个级联选项，支持两种格式：a.数字1,数字2；b.具体选项路径，如：陆路>汽车类>卡车，水路>船类
        :param search_for: 搜索关键字
        :param instance: Test类实例
        :param auto_unfold: 是否自动展开输入框
        :return:
        """
        # 检查是否自动展开
        if not auto_unfold:
            input_wrapper.click()
        # 截屏并保存PNG
        save_png(instance)

        try:
            if search_for is not None:
                search_input_ele = input_wrapper.find_element(By.XPATH, self.search_input_loc)
                search_input_ele.send_keys(search_for)
                ul_ele = self.driver.find_element(By.XPATH, self.ul_loc)
                lis = ul_ele.find_elements(By.TAG_NAME, 'li')

                # 点击指定的li
                if options.strip() == 'all':
                    target_lis = lis
                else:
                    idxs = [int(idx) - 1 for idx in options.strip().replace('，', ',').split(',')]
                    target_lis = [lis[i] for i in idxs]

                for li in target_lis:
                    li.find_element(By.XPATH, self.check_loc).click()
            else:
                cleaned_options = options.strip().replace('，', ',').split(',')
                for option in cleaned_options:
                    option_parser = [node.strip() for node in option.split('>')]
                    for level, op_name in enumerate(option_parser, 1):
                        # 拼接出每级panel的ul
                        current_ul_loc = self.panels_wrap_loc + self.panel_structure * level + '/ul'
                        ul_ele = self.driver.find_element(By.XPATH, current_ul_loc)
                        current_li = [li for li in ul_ele.find_elements(By.TAG_NAME, 'li') if op_name in li.text][0]
                        if level == len(option_parser):
                            check_ele = current_li.find_element(By.XPATH, self.check_loc)
                            check_ele.click()
                        else:
                            current_li.click()
        except:
            raise
        finally:
            self._back_up(input_wrapper)


class Date(BasicComponent):
    """ 日期 """

    input_loc = './/div[1]/input'
    clear_icon_loc = ".//div[1]/i"

    def input_box(self, input_wrapper: WebElement, content=None):
        """
        :param input_wrapper: 包囊组件的父级WebElement
        :param content: 输入内容
        :return:
        """
        input_ = input_wrapper.find_element(By.XPATH, self.input_loc)

        # 先尝试清空值
        try:
            self.hover(input_wrapper)
            clear_icon = input_wrapper.find_element(By.XPATH, self.clear_icon_loc)
            clear_icon.click()
        except NoSuchElementException:
            logger.info('值本来就是空的，无需清空')
            pass

        if content is not None:
            cleaned_content = content.replace('"', '').replace('“', '')
            input_.send_keys(cleaned_content)


class Time(Date):
    """ 时间 """


class DateTime(Date):
    """ 日期时间 """


class DateRange(Date):
    """ 时间范围 """


class RichText(BasicComponent):
    """ 富文本 """

    def input_box(self, input_wrapper: WebElement, content=None):
        """
        :param input_wrapper:  包囊组件的父级WebElement
        :param content: 输入内容，eg：null;  eg: default; eg: 自定义的内容
        :return:
        """
        content = content.strip()
        if content == 'null':
            return
        try:
            if isinstance(content, str) and content != 'default':
                # 插入文字
                self._insert_content(input_wrapper, content)
            elif content == 'default':
                # 插入内置内容，包含：文字、图片、表格、代码段
                self._insert_content(input_wrapper)
            else:
                raise Exception(f'【{content}】不支持')
        except Exception:
            raise
        finally:
            self.driver.switch_to.default_content()

    def _insert_content(self, field, content="default"):
        # 构建复杂内容的 HTML 字符串
        if content != "default":
            body_content = f'<p>{content}</p>'
        else:
            demo_txt_path = ROOT_PATH + '/assets/富文本模板.txt'
            with open(demo_txt_path, mode='r', encoding='utf8') as f:
                default_content = f.read()
            body_content = default_content

        final_content = f"""<!DOCTYPE html>\n<html>\n<head>\n</head>\n<body>\n{body_content}\n</body>\n</html>"""

        # 点击源代码按钮
        source_button_loc = './/button[@title="源代码"]'
        source_button = field.find_element(By.XPATH, source_button_loc)
        source_button.click()

        # 弹窗中填写html源码
        tox_textarea_loc = '//*[contains(@id, "dialog-describe")]/div/div/div/div/textarea'
        tox_textarea = self.driver.find_element(By.XPATH, tox_textarea_loc)
        tox_textarea.clear()
        tox_textarea.send_keys(final_content)

        # 关闭弹窗
        save_button_loc = '//div[@class="tox-dialog__footer"]/div[2]/button[@title="保存"]'
        save_button = tox_textarea.find_element(By.XPATH, save_button_loc)
        save_button.click()


class Switcher(BasicComponent):
    """ 开关 """


# ==================================================================================================================
# >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 高级字段 <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<

class File(AdvancedComponent):
    """ 附件 """

    # 上传文件的input
    file_input_loc = './/input[@type="file"]'

    def input_box(self, input_wrapper: WebElement, files: str, sleep_sec=1, instance=None):
        """
        :param self:
        :param input_wrapper: 包囊组件的父级WebElement， 必须是input[@type="file"]的父级
        :param files: 文件名称，用逗号分隔。 注意：需要把文件放到根目录的assets文件夹下
        :param sleep_sec: 上传间隔秒数
        :param instance: Test类实例
        :return:
        """
        input_ele = input_wrapper.find_element(By.XPATH, self.file_input_loc)
        files = [ROOT_PATH + f'/assets/{f_name.strip()}' for f_name in files.replace('，', ',').split(',')]
        for f_path in files:
            input_ele.send_keys(f_path)
            # 截屏并保存PNG
            save_png(instance)
            self.key_sleep(sleep_sec)


class Link(AdvancedComponent):
    """ 链接 """


class UserGroup(AdvancedComponent):
    """ 单选用户组"""


class MultiUserGroup(AdvancedComponent):
    """ 多选用户组 """


class Cmdb(AdvancedComponent):
    """ 单选CMDB业务 """


class MultiCmdb(AdvancedComponent):
    """ 多选CMDB业务 """


class SubTable(AdvancedComponent):
    """ 子表单 """


class Task(AdvancedComponent):
    """ 任务 """
